#include "common.h"
#include "counter.h"
#include "Vbpu_test.h"
#include "trace.h"
#include <queue>
#define RUN_CYCLE 4000000
#define MAX_WAVE 1000000
#define TRACE 0
#define BLOCK_SIZE 2
Vbpu_test top;
VerilatedFstC *tfp;
static uint64_t dump_time = 0;
uint64_t op_cnt = 0;

void dump()
{
#if TRACE
    if (dump_time < MAX_WAVE)
    {
        tfp->dump(dump_time++);
    }
#endif
}

void run()
{
    top.clk = 0;
    top.eval();
    dump();
    top.clk = 1;
    top.eval();
    dump();
}

void reset()
{
    top.rst = 1;
    run();
    run();
    run();
    top.rst = 0;
}
enum
{
    NONE = 0,
    Branch,
    Jump,
    Call,
    Ret,
    CallRet
};
typedef struct
{
    bool valid;
    bool taken;
    word_t target;
    uint8_t type;

    uint8_t btb_sc;
    bool ras_valid;
    uint32_t ras_pointer;
    word_t ras_entry;
} BPU_Res;
typedef struct
{
    OP *op;
    bool bpred_miss;
    BPU_Res bpu_res;
} FrontendOP;
std::queue<std::tuple<FrontendOP, word_t>> ops;
word_t pc = 0x80000000;
bool wrong_path = false;
word_t correct_pc;
void execute_bru_op(FrontendOP &fop)
{

    bool taken = fop.op->trace.branch_taken;
    word_t addr = fop.op->trace.pc;
    word_t target = fop.op->trace.branch_pc;
    uint8_t type = NONE;
    auto trace = fop.op->trace;
    auto opcode = fop.op->trace.opcode;
    if (opcode == OP_CBR)
    {
        type = Branch;
    }
    else if (opcode == OP_JMP)
    {
        int8_t rs1 = trace.src[0];
        int8_t rd = trace.dst;
        bool rs1_link = rs1 == 1 || rs1 == 5;
        bool rd_link = rd == 1 || rd == 5;
        bool jal = rs1 == -1;
        type = Jump;
        if (jal && rd_link)
        {
            type = Call;
        }
        else
        {
            if (!rd_link && rs1_link)
            {
                type = Ret;
            }
            else if (rd_link && !rs1_link)
            {
                type = Call;
            }
            else if (rd_link && rs1_link && rd != rs1)
            {
                type = CallRet;
            }
            else if (rd_link && rs1_link && rd == rs1)
            {
                type = Call;
            }
        }
    }

    BPU_Res correct_res = {0};
    if (fop.bpred_miss || type != NONE)
    {
        if (fop.bpu_res.valid)
        {
            correct_res = fop.bpu_res;
        }
        else
        {
            switch (type)
            {
            case Jump:
                break;
            case Branch:
                correct_res.btb_sc = taken ? 2 : 1;
                break;
            case Call:
            case Ret:
            case CallRet:
                // LOG("Update pc 0x%08lx target 0x%08lx type %d index %lu at %lu", addr, target, type, (addr - 0x80000000) / 4, dump_time);
                break;
            default:
                LOG("[ERROR] should not update NONE branch instruction to frontend");
                break;
            }
        }
        correct_res.taken = taken;
        correct_res.target = target;
        correct_res.valid = true;
        correct_res.type = type;
    }
    if (fop.bpred_miss)
    {
        // frontend->redirect(addr, correct_res);
        pc = correct_res.taken ? correct_res.target : addr + 4;
        wrong_path = false;
        if (correct_pc != pc)
        {
            LOG("[ERROR] redirect target not correct!");
            return;
        }
    }
    else if (type != NONE)
    {
        correct_res.ras_valid = false;
        // frontend->update(addr, correct_res);
    }
    if (fop.bpred_miss || type != NONE)
    {
    }
    // if (correct_res.ras_valid)
    // {
    //     LOG("Restore RAS top 0x%08lx pointer 0x%08x", correct_res.ras_entry, correct_res.ras_pointer);
    // }
    // LOG("Update pc 0x%08lx target 0x%08lx type %d index %lu at %lu", addr, correct_res.target, correct_res.type, (addr-0x80000000)/4, dump_time);
    top.update_pc = addr;
    top.update_valid = correct_res.valid;
    top.update_branch_type = correct_res.type;
    top.update_taken = correct_res.taken;
    top.update_ras_valid = correct_res.ras_valid;
    top.update_ras_pointer = correct_res.ras_pointer;
    top.update_bi_cnt = correct_res.btb_sc;
    top.update_target = correct_res.target;
    top.update_ras_top = correct_res.ras_entry;
}

int main()
{
    Counter ras_miss("ras_miss"), ras_hit("ras_hit");
    Counter bimodal_miss("bimodal_miss"), bimodal_hit("bimodal_hit");
    Counter jmp_miss("jmp_miss"), jmp_hit("jmp_hit");
    Counter total_miss("total_miss"), total_hit("total_hit");
    Counter unpredict_miss("unpredict_miss"), unpredict_hit("unpredict_hit");
    Counter l1btb_hit("l1btb_hit"), l1btb_miss("l1btb_miss"), l1btb_hit_way0("l1btb_hit_way0");
    Counter error_predict_miss("error_predict_miss"), l1btb_miss_but_bpred_hit("l1btb_miss_but_bpred_hit");
    Counter l1btb_hit_other_way("l1btb_hit_other_way");
#if TRACE
    Verilated::traceEverOn(true);
    tfp = new VerilatedFstC();
    top.trace(tfp, 0);
    tfp->open("Vbpu_test.fst");
#endif
    init_trace(std::string("./trace/op.trace"));
    while (!ops.empty())
    {
        ops.pop();
    }
    uint64_t test_cycle = RUN_CYCLE;
    uint64_t cycle = 0;
    top.update_valid = 0;
    top.i_valid = 0;
    reset();
    while (!top.i_ready)
    {
        run();
    }

    bool finish = false;
    while (cycle < test_cycle && !finish)
    {
        top.update_valid = 0;
        if (!ops.empty())
        {
            auto opf = ops.front();
            auto fop = std::get<0>(opf);
            auto pcycle = std::get<1>(opf);
            if (pcycle + 8 < cycle)
            {
                execute_bru_op(fop);
                free_op(fop.op);
                ops.pop();
            }
        }
        top.i_valid = 1;
        top.i_pc = pc;
        if (top.update_valid)
        {
            run();
            cycle++;
            continue;
        }
        run();

        BPU_Res bpu_res[BLOCK_SIZE] = {0};
        size_t num_to_fetch = BLOCK_SIZE;
        if (top.o_res_valid)
        {
            bpu_res[top.o_res_offset].valid = 1;
            bpu_res[top.o_res_offset].taken = top.o_res_taken;
            bpu_res[top.o_res_offset].target = top.o_res_target;
            bpu_res[top.o_res_offset].type = top.o_res_branch_type;
            bpu_res[top.o_res_offset].btb_sc = top.o_res_bi_cnt;
            bpu_res[top.o_res_offset].ras_valid = top.o_res_ras_valid;
            bpu_res[top.o_res_offset].ras_pointer = top.o_res_ras_pointer;
            bpu_res[top.o_res_offset].ras_entry = top.o_res_ras_top;
        }
        if (top.o_res_valid & (top.o_res_taken | top.o_res_multi_branch))
        {
            num_to_fetch = top.o_res_offset + 1;
            assert(num_to_fetch <= BLOCK_SIZE);
        }
        bool has_branch_op = false;
        for (size_t i = 0; i < num_to_fetch; i++)
        {
            word_t snpc = pc + 4;
            word_t pnpc = snpc;
            if (bpu_res[i].valid && bpu_res[i].taken)
            {
                pnpc = bpu_res[i].target;
            }
            if (!wrong_path)
            {
                auto op = read_op();
                op_cnt++;
                if (op->valid == false)
                {
                    free_op(op);
                    if (ops.empty())
                    {
                        finish = true;
                        break;
                    }
                    continue;
                }
                if (op->trace.pc != pc)
                {
                    LOG("[Frontend] read op %lu in wrong path.", op->id);
                }
                word_t dnpc = op->trace.branch_taken ? op->trace.branch_pc : (op->trace.pc + 4);
                bool bpred_hit = dnpc == pnpc;

                if (op->trace.opcode == OP_CBR || op->trace.opcode == OP_JMP)
                {
                    if (bpred_hit)
                    {
                        total_hit++;
                    }
                    else
                    {
                        total_miss++;
                    }

                    if (top.o_res_valid && top.o_res_offset == i)
                    {

                        l1btb_hit++;
                        if (i == 0)
                        {
                            l1btb_hit_way0++;
                        }
                    }
                    else
                    {
                        if (top.o_res_valid)
                        {
                            l1btb_hit_other_way++;
                        }
                        if (bpred_hit)
                        {
                            l1btb_miss_but_bpred_hit++;
                        }
                        else
                        {
                            l1btb_miss++;
                            // LOG("BTB_MISS pc 0x%08lx offset %d at %lu", pc, top.o_res_offset, dump_time);
                        }
                    }

                    has_branch_op = true;
                }
                if (bpred_hit)
                {
                    if (bpu_res[i].valid)
                    {
                        switch (bpu_res[i].type)
                        {
                        case Call:
                            // LOG("[RAS] ras push at %lu-%lu pc %08lx dnpc %08lx pnpc %08lx", cycle, dump_time, pc, dnpc, pnpc);
                        case Jump:
                            jmp_hit++;
                            // SPDLOG_TRACE("[BranchPredict] BTB hit pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                            break;
                        case Branch:
                            bimodal_hit++;
                            // SPDLOG_TRACE("[BranchPredict] bimodal hit pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                            break;
                        case Ret:
                        case CallRet:
                            ras_hit++;
                            // LOG("[RAS] ras hit  at %lu-%lu pc %08lx dnpc %08lx pnpc %08lx", cycle, dump_time, pc, dnpc, pnpc);
                            // SPDLOG_TRACE("[BranchPredict] ras hit pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                            break;
                        default:
                            LOG("[ERROR0] should not make NONE branch instruction into bpu at cycle %lu-%lu", cycle, dump_time);
                            break;
                        }
                    }
                    else if (op->trace.opcode == OP_CBR || op->trace.opcode == OP_JMP)
                    {
                        unpredict_hit++;
                    }
                }
                else
                {
                    wrong_path = true;
                    correct_pc = dnpc;
                    if (bpu_res[i].valid)
                    {
                        switch (bpu_res[i].type)
                        {
                        case Call:
                            // LOG("[RAS] ras push at %lu-%lu pc %08lx dnpc %08lx pnpc %08lx", cycle, dump_time, pc, dnpc, pnpc);
                        case Jump:
                            jmp_miss++;
                            // SPDLOG_TRACE("[BranchPredict] BTB miss pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                            break;
                        case Branch:
                            bimodal_miss++;
                            // SPDLOG_TRACE("[BranchPredict] bimodal miss pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                            break;
                        case Ret:
                        case CallRet:
                            ras_miss++;
                            // LOG("[RAS] ras miss at %lu-%lu pc %08lx dnpc %08lx pnpc %08lx", cycle, dump_time, pc, dnpc, pnpc);
                            // SPDLOG_TRACE("[BranchPredict] ras miss pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                            break;
                        default:
                            LOG("bpu_res[%lu].valid %d", i, bpu_res[i].valid);
                            LOG("[ERROR1] should not make NONE branch instruction into bpu at cycle %lu dump %lu", cycle, dump_time);
                            break;
                        }
                    }
                    else if (op->trace.opcode == OP_CBR || op->trace.opcode == OP_JMP)
                    {
                        unpredict_miss++;
                        // SPDLOG_TRACE("[BranchPredict] un-predict miss pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                    }
                    else
                    {
                        error_predict_miss++;
                        // SPDLOG_TRACE("[BranchPredict] error-predict miss pc:{:08x} dnpc:{:08x} pnpc{:08x}", state.pc, dnpc, pnpc);
                    }
                }

                if (bpred_hit)
                {
                    free_op(op);
                }
                else
                {
                    wrong_path = true;
                    correct_pc = dnpc;

                    FrontendOP fop;
                    fop.op = op;
                    fop.bpred_miss = !bpred_hit;
                    fop.bpu_res = bpu_res[i];
                    ops.push({fop, cycle});
                }
            }
            else
            {
            }
            pc = pnpc;
        }
        cycle++;
    }
#if TRACE
    tfp->close();
    delete tfp;
#endif
    LOG("total op: %lu", op_cnt);
    LOG("l1btb     hit rate %.03f%%", 100.0f * l1btb_hit / (l1btb_hit + l1btb_miss));
    LOG("ras       hit rate %.03f%%", 100.0f * ras_hit / (ras_hit + ras_miss));
    LOG("bimodal   hit rate %.03f%%", 100.0f * bimodal_hit / (bimodal_hit + bimodal_miss));
    LOG("jmp       hit rate %.03f%%", 100.0f * jmp_hit / (jmp_hit + jmp_miss));
    LOG("unpredict hit rate %.03f%%", 100.0f * unpredict_hit / (unpredict_hit + unpredict_miss));
    LOG("total     hit rate %.03f%%", 100.0f * total_hit / (total_hit + total_miss));
    LOG("Average Fetch Speed %.03f instructions per cycle", (float)op_cnt / cycle);
    Counter::print_counters();
    return 0;
}